const shader = require('../shader/index');

const color = new Float32Array([0.5, 0.5, 1.0, 1.0]);
// Size of off screen
const OFFSCREEN_WIDTH = 1024;
const OFFSCREEN_HEIGHT = 512;

module.exports = class ConstColorLayer {
    constructor(option) {
        // init config
        this.id = option.id;
        this.gl = option.gl;
        const gl = option.gl;
        this.globe = option.globe;
        this.radius = option.radius;

        // init shader for frameBuffer
        const programFrameBuffer = gl.createProgram();
        this.programFrameBuffer = programFrameBuffer;
        const fragmentShaderFrameBuffer = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fragmentShaderFrameBuffer, shader.color.fragmentSource);
        gl.compileShader(fragmentShaderFrameBuffer);
        gl.attachShader(programFrameBuffer, fragmentShaderFrameBuffer);
        const vertexShaderFrameBuffer = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertexShaderFrameBuffer, shader.color.vertexSource);
        gl.compileShader(vertexShaderFrameBuffer);
        gl.attachShader(programFrameBuffer, vertexShaderFrameBuffer);
        gl.linkProgram(programFrameBuffer);

        // get attribLocation
        this.attribLocationFrameBuffer = {
            a_Position: gl.getAttribLocation(programFrameBuffer, 'a_Position'),
            a_Normal: gl.getAttribLocation(programFrameBuffer, 'a_Normal')
        }

        // get uniformLocation
        this.uniformLocationFrameBuffer = {
            u_ModelMatrix: gl.getUniformLocation(programFrameBuffer, 'u_ModelMatrix'),
            u_ViewMatrix: gl.getUniformLocation(programFrameBuffer, 'u_ViewMatrix'),
            u_ProjMatrix: gl.getUniformLocation(programFrameBuffer, 'u_ProjMatrix'),
            u_NormalMatrix: gl.getUniformLocation(programFrameBuffer, 'u_NormalMatrix'),
            u_LightColor: gl.getUniformLocation(programFrameBuffer, 'u_LightColor'),
            u_LightPosition: gl.getUniformLocation(programFrameBuffer, 'u_LightPosition'),
            u_AmbientColor: gl.getUniformLocation(programFrameBuffer, 'u_AmbientColor'),
            u_Color: gl.getUniformLocation(programFrameBuffer, 'u_Color')
        }

        // init shader for halo
        const programHalo = gl.createProgram();
        this.programHalo = programHalo;
        const fragmentShaderHalo = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fragmentShaderHalo, shader.colorTexture.fragmentSource);
        gl.compileShader(fragmentShaderHalo);
        gl.attachShader(programHalo, fragmentShaderHalo);
        const vertexShaderHalo = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertexShaderHalo, shader.colorTexture.vertexSource);
        gl.compileShader(vertexShaderHalo);
        gl.attachShader(programHalo, vertexShaderHalo);
        gl.linkProgram(programHalo);

        // get attribLocation
        this.attribLocationHalo = {
            a_Position: gl.getAttribLocation(programHalo, 'a_Position'),
            a_TexCoord: gl.getAttribLocation(programHalo, 'a_TexCoord')
        }

        // get uniformLocation
        this.uniformLocationHalo = {
            u_Width: gl.getUniformLocation(programHalo, 'u_Width'),
            u_Height: gl.getUniformLocation(programHalo, 'u_Height'),
            u_Sampler: gl.getUniformLocation(programHalo, 'u_Sampler')
        }

        // init shader for plane
        const programPlane = gl.createProgram();
        this.programPlane = programPlane;
        const fragmentShaderPlane = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fragmentShaderPlane, shader.texture.fragmentSource);
        gl.compileShader(fragmentShaderPlane);
        gl.attachShader(programPlane, fragmentShaderPlane);
        const vertexShaderPlane = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertexShaderPlane, shader.texture.vertexSource);
        gl.compileShader(vertexShaderPlane);
        gl.attachShader(programPlane, vertexShaderPlane);
        gl.linkProgram(programPlane);

        // get attribLocation
        this.attribLocationPlane = {
            a_Position: gl.getAttribLocation(programPlane, 'a_Position'),
            a_TexCoord: gl.getAttribLocation(programPlane, 'a_TexCoord')
        }

        // get uniformLocation
        this.uniformLocationPlane = {
            u_Width: gl.getUniformLocation(programPlane, 'u_Width'),
            u_Color: gl.getUniformLocation(programPlane, 'u_Color'),
            u_Height: gl.getUniformLocation(programPlane, 'u_Height'),
            u_Sampler: gl.getUniformLocation(programPlane, 'u_Sampler')
        }

        const fboVertexBuffers = initVertexBuffers(gl, this.radius);
        const planeVertexBuffers = initVertexBuffersForPlane(gl);
        const frameBuffer = initFramebufferObject(gl, this.globe.width, this.globe.height);
        const frameBuffer1 = initFramebufferObject(gl, this.globe.width, this.globe.height);

        this.buffer = {
            frameBuffer: frameBuffer,
            frameBuffer1: frameBuffer1,

            planeVertexBuffer: planeVertexBuffers.vertexBuffer,
            planeTexCoordBuffer: planeVertexBuffers.texCoordBuffer,
            planeIndexBuffer: planeVertexBuffers.indexBuffer,
            planeN: planeVertexBuffers.n,

            positionVertexBuffer: fboVertexBuffers.positionVertexBuffer,
            texCoordVertexBuffer: fboVertexBuffers.texCoordVertexBuffer,
            normalVertexBuffer: fboVertexBuffers.normalVertexBuffer,
            indexBuffer: fboVertexBuffers.indexBuffer,
            n: fboVertexBuffers.n
        };
    }

    resize() {
        this.buffer.frameBuffer = initFramebufferObject(this.gl, this.globe.width, this.globe.height);
        this.buffer.frameBuffer1 = initFramebufferObject(this.gl, this.globe.width, this.globe.height);
    }

    render(config) {
        const modelMatrix = config.modelMatrix;
        const viewMatrix = config.viewMatrix;
        const projMatrix = config.projMatrix;
        const normalMatrix = config.normalMatrix;

        const gl = this.gl;
        gl.useProgram(this.programFrameBuffer);

        gl.bindFramebuffer(gl.FRAMEBUFFER, this.buffer.frameBuffer);              // Change the drawing destination to FBO
        gl.viewport(0, 0, this.globe.width, this.globe.height); // Set a viewport for FBO

        gl.clearColor(0.0, 0.0, 0.0, 0.0); // Set clear color (the color is slightly changed)
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);  // Clear FBO

        // start globe render
        gl.enable(gl.BLEND);
        // gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
        // gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
        gl.depthMask(false);
        // gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);

        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer.positionVertexBuffer);
        gl.vertexAttribPointer(this.attribLocationFrameBuffer.a_Position, 3, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(this.attribLocationFrameBuffer.a_Position);

        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer.normalVertexBuffer);
        gl.vertexAttribPointer(this.attribLocationFrameBuffer.a_Normal, 3, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(this.attribLocationFrameBuffer.a_Normal);

        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.buffer.indexBuffer);

        gl.uniform3f(this.uniformLocationFrameBuffer.u_AmbientColor, 0.2, 0.2, 0.2);
        gl.uniform3f(this.uniformLocationFrameBuffer.u_LightColor, 1.0, 1.0, 1.0);
        gl.uniform3f(this.uniformLocationFrameBuffer.u_LightPosition, 0.0, 0.0, 20.0);

        gl.uniform4fv(this.uniformLocationFrameBuffer.u_Color, color);
        gl.uniformMatrix4fv(this.uniformLocationFrameBuffer.u_ModelMatrix, false, modelMatrix);
        gl.uniformMatrix4fv(this.uniformLocationFrameBuffer.u_ViewMatrix, false, viewMatrix);
        gl.uniformMatrix4fv(this.uniformLocationFrameBuffer.u_ProjMatrix, false, projMatrix);
        gl.uniformMatrix4fv(this.uniformLocationFrameBuffer.u_NormalMatrix, false, normalMatrix);

        gl.drawElements(gl.TRIANGLES, this.buffer.n, gl.UNSIGNED_SHORT, 0);

        gl.bindFramebuffer(gl.FRAMEBUFFER, null);        // Change the drawing destination to color buffer
        // gl.viewport(0, 0, this.globe.width, this.globe.height);  // Set the size of viewport back to that of <canvas>

        for (let i = 0; i < 3; i++) {        
            // for halo
            gl.useProgram(this.programHalo);

            gl.bindFramebuffer(gl.FRAMEBUFFER, i % 2 === 0 ? this.buffer.frameBuffer1 : this.buffer.frameBuffer);              // Change the drawing destination to FBO
            gl.viewport(0, 0, this.globe.width, this.globe.height); // Set a viewport for FBO

            gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer.planeVertexBuffer);
            gl.vertexAttribPointer(this.attribLocationHalo.a_Position, 3, gl.FLOAT, false, 0, 0);
            gl.enableVertexAttribArray(this.attribLocationHalo.a_Position);

            gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer.planeTexCoordBuffer);
            gl.vertexAttribPointer(this.attribLocationHalo.a_TexCoord, 2, gl.FLOAT, false, 0, 0);
            gl.enableVertexAttribArray(this.attribLocationHalo.a_TexCoord);

            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.buffer.planeIndexBuffer);

            gl.activeTexture(gl.TEXTURE0);
            gl.bindTexture(gl.TEXTURE_2D, i % 2 === 0 ? this.buffer.frameBuffer.texture : this.buffer.frameBuffer1.texture);

            gl.uniform1i(this.uniformLocationHalo.u_Sampler, 0);
            gl.uniform1f(this.uniformLocationHalo.u_Width, this.globe.width);
            gl.uniform1f(this.uniformLocationHalo.u_Height, this.globe.height);

            // gl.blendFunc(gl.SRC_ALPHA, gl.DST_ALPHA);

            gl.clearColor(0.0, 0.0, 0.0, 0.0); // Set clear color (the color is slightly changed)
            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);  // Clear FBO

            gl.drawElements(gl.TRIANGLES, this.buffer.planeN, gl.UNSIGNED_SHORT, 0);
        }
        gl.depthMask(true);
        // end globe render

        gl.bindFramebuffer(gl.FRAMEBUFFER, null);        // Change the drawing destination to color buffer
        gl.viewport(0, 0, this.globe.width, this.globe.height);  // Set the size of viewport back to that of <canvas>

        gl.useProgram(this.programPlane);

        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer.planeVertexBuffer);
        gl.vertexAttribPointer(this.attribLocationPlane.a_Position, 3, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(this.attribLocationPlane.a_Position);

        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer.planeTexCoordBuffer);
        gl.vertexAttribPointer(this.attribLocationPlane.a_TexCoord, 2, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(this.attribLocationPlane.a_TexCoord);

        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.buffer.planeIndexBuffer);

        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, this.buffer.frameBuffer1.texture);

        gl.uniform1i(this.uniformLocationPlane.u_Sampler, 0);
        gl.uniform1f(this.uniformLocationPlane.u_Width, this.globe.width);
        gl.uniform1f(this.uniformLocationPlane.u_Height, this.globe.height);
        gl.uniform4fv(this.uniformLocationPlane.u_Color, color);

        gl.blendFunc(gl.SRC_ALPHA, gl.DST_ALPHA);

        gl.drawElements(gl.TRIANGLES, this.buffer.planeN, gl.UNSIGNED_SHORT, 0);

        gl.disable(gl.BLEND);
    }
}

function initVertexBuffers(gl, radius) {
    let latitudeBands = 60;
    let longitudeBands = 60;

    let vertices = [];
    let normals = [];

    for (let latNumber = 0; latNumber <= latitudeBands; latNumber++) {
        let theta = latNumber * Math.PI / latitudeBands;
        let sinTheta = Math.sin(theta);
        let cosTheta = Math.cos(theta);

        for (let longNumber = 0; longNumber <= longitudeBands; longNumber++) {
            let phi = longNumber * 2 * Math.PI / longitudeBands;
            let sinPhi = Math.sin(phi);
            let cosPhi = Math.cos(phi);

            let x = cosPhi * sinTheta;
            let y = cosTheta;
            let z = sinPhi * sinTheta;
            let s = 1 - (longNumber / longitudeBands);
            let t = 1 - (1 - (latNumber / latitudeBands));

            normals.push(x);
            normals.push(y);
            normals.push(z);
            vertices.push(radius * x);
            vertices.push(radius * y);
            vertices.push(radius * z);
        }
    }

    let indices = [];
    for (let latNumber = 0; latNumber < latitudeBands; latNumber++) {
        for (let longNumber = 0; longNumber < longitudeBands; longNumber++) {
            let first = (latNumber * (longitudeBands + 1)) + longNumber;
            let second = first + longitudeBands + 1;
            indices.push(first);
            indices.push(second);
            indices.push(first + 1);

            indices.push(second);
            indices.push(second + 1);
            indices.push(first + 1);
        }
    }

    const positionVertexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionVertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

    const normalVertexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, normalVertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);

    const indexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);

    return {
        positionVertexBuffer: positionVertexBuffer,
        normalVertexBuffer: normalVertexBuffer,
        indexBuffer: indexBuffer,
        n: indices.length
    };
}

function initVertexBuffersForPlane(gl) {
    // Create face
    //  v1------v0
    //  |        | 
    //  |        |
    //  |        |
    //  v2------v3

    // Vertex coordinates
    var vertices = new Float32Array([
        1.0, 1.0, 0.0, -1.0, 1.0, 0.0, -1.0, -1.0, 0.0, 1.0, -1.0, 0.0    // v0-v1-v2-v3
    ]);

    // Texture coordinates
    var texCoords = new Float32Array([1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);

    // Indices of the vertices
    var indices = new Uint16Array([0, 1, 2, 0, 2, 3]);

    const vertexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

    const texCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords), gl.STATIC_DRAW);

    const indexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);

    // Unbind the buffer object
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

    return {
        vertexBuffer: vertexBuffer,
        texCoordBuffer: texCoordBuffer,
        indexBuffer: indexBuffer,
        n: indices.length
    };
}

function initFramebufferObject(gl, width, height) {
    let framebuffer, texture, depthBuffer;

    // Define the error handling function
    const error = function () {
        if (framebuffer) gl.deleteFramebuffer(framebuffer);
        if (texture) gl.deleteTexture(texture);
        if (depthBuffer) gl.deleteRenderbuffer(depthBuffer);
        return null;
    }

    // Create a frame buffer object (FBO)
    framebuffer = gl.createFramebuffer();
    if (!framebuffer) {
        console.log('Failed to create frame buffer object');
        return error();
    }

    // Create a texture object and set its size and parameters
    texture = gl.createTexture(); // Create a texture object
    if (!texture) {
        console.log('Failed to create texture object');
        return error();
    }
    gl.bindTexture(gl.TEXTURE_2D, texture); // Bind the object to target
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

    // make sure we can render it even if it's not a power of 2
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

    framebuffer.texture = texture; // Store the texture object

    // Create a renderbuffer object and Set its size and parameters
    depthBuffer = gl.createRenderbuffer(); // Create a renderbuffer object
    if (!depthBuffer) {
        console.log('Failed to create renderbuffer object');
        return error();
    }
    gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); // Bind the object to target
    gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);

    // Attach the texture and the renderbuffer object to the FBO
    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
    gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);

    // Check if FBO is configured correctly
    let e = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
    if (gl.FRAMEBUFFER_COMPLETE !== e) {
        console.log('Frame buffer object is incomplete: ' + e.toString());
        return error();
    }

    // Unbind the buffer object
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.bindTexture(gl.TEXTURE_2D, null);
    gl.bindRenderbuffer(gl.RENDERBUFFER, null);

    return framebuffer;
}
